local ROT = require 'rot'
local RNG = ROT.RNG

local Scene = ROT.Class:extend()

local Builder = require 'game.builder'
local Light = require 'game.light'
local System = require 'game.system'

local Cloud = require 'game.proto.cloud'
local Effect = require 'game.proto.effect'
local Feature = require 'game.proto.feature'
local Mob = require 'game.proto.mob'
local Item = require 'game.proto.item'

Scene.innerWidth = 40
Scene.innerHeight = 40
Scene.fovRadius = 7
Scene.defaultFeature = 'wall'
Scene.outdoors = false

local nothing = { priority = math.huge }

function Scene:init(game, previous, depth)
    self.animate = 0
    self.ambientLight = { 120, 120, 120 }
    self.game = game
    self.previous = previous
    self.depth = depth or self.previous and self.previous.depth + 1 or 1
    self.player = game.player
    self.light = Light(self)
    self.seen = ROT.Type.Grid() -- explored cells
    self.field = ROT.Type.Grid() -- visible cells
    self.feature = ROT.Type.Grid() -- static features (wall, floor, lava)
    self.mob = ROT.Type.Grid() -- mobile entities (monsters, allies)
    self.cloud = ROT.Type.Grid() -- clouds (flame, smoke)
    self.stack = ROT.Type.Grid() -- stacks of items on floor
    self.interactive = ROT.Type.Grid() -- interactive cells (stairs, switches)
    self.featureData = ROT.Type.Grid() -- stairway ids, decals
    self.visibleMobs = {}

    self.upstairs = {}
    self.downstairs = {}
    self.branches = {}

    self.builder = Builder(self)
    self:build(self.depth)

    -- cleanup
    for i, x, y, v in self.stack:each() do
        local f = self:getFeature(x, y)
        if f and f.obstruct then self.stack:removeCell(x, y) end
    end
    -- TODO: sanity checks

    self.light:compute()

    self.fov=ROT.FOV.Precise:new(function (fov, x, y)
        return not self:getFeature(x, y).obscure
            and (not self:getCloud(x, y).obscure
                or math.abs(game.player.x - x) < 2 and math.abs(game.player.y - y) < 2)
    end)

end

function Scene:erode(...) return self.builder:erode(...) end
function Scene:expand(...) return self.builder:expand(...) end
function Scene:decimate(...) return self.builder:decimate(...) end
function Scene:fabricate(...) return self.builder:fabricate(...) end
function Scene:with(...) return self.builder:with(...) end

function Scene:build(depth) -- override in subclass
    local m = ROT.Map.Arena:new(self.innerWidth, self.innerHeight)
    m:create(function (x, y, val)
        self.feature:setCell(x, y, val == 1 and 'wall' or 'floor')
    end)
end

function Scene:getCloud(x, y)
    return self.cloud:getCell(x, y) or nothing
end

function Scene:putCloud(x, y, name, cloud)
    if self:getFeature(x, y).obstruct then return end

    cloud = type(name) == 'string' and
            setmetatable(cloud or {}, { __index = Cloud[name] })
            or name

    if self:getCloud(x, y).priority < cloud.priority then return end

    self.cloud:setCell(x, y, cloud)
    return cloud
end


function Scene:getFeature(x, y)
    local name = self.feature:getCell(x, y)
    return name and Feature[name] or nothing
end

function Scene:getMob(x, y)
    return self.mob:getCell(x, y)
end


function Scene:getItem(x, y)
    local stack = self.stack:getCell(x, y)
    return stack and stack[#stack]
end

function Scene:putItem(x, y, name, item)
    item = type(name) == 'string' and
        setmetatable(item or {}, { __index = Item[name] })
        or name
    local stack = self.stack:getCell(x, y)
    if not stack then
        stack = {}
        self.stack:setCell(x, y, stack)
    end
    stack[#stack + 1] = item
    return item
end

function Scene:getTile(x, y)
    return self:getMob(x, y) or self.cloud:getCell(x, y) or self:getItem(x, y) or self:getFeature(x, y)
end

function Scene:spawn(x, y, name, mob)
    mob = setmetatable(mob or {}, { __index = Mob[name] })
    self.mob:setCell(x, y, mob)
    mob.x, mob.y = x, y
    mob.stuff = mob.stuff or {}
    mob.maxHealth = mob.str * 10
    mob.maxStamina = mob.dex * 10
    mob.health = mob.maxHealth
    mob.stamina = mob.maxStamina
    mob.healthRegen = mob.healthRegen or 0.1
    mob.staminaRegen = mob.staminaRegen or 0.1
    mob.delay = mob.delay or 0
    mob.attention = mob.attention or 100 -- attention span
    mob.distraction = 0

    if type(mob.unarmed) == 'string' then
        mob.unarmed = setmetatable({}, { __index = Item[mob.unarmed] })
    end
    if type(mob.weapon) == 'string' then
        self:equip(mob, mob.weapon)
    end
    if type(mob.armor) == 'string' then
        self:equip(mob, mob.armor)
    end
    mob.weapon = mob.weapon or mob.unarmed
    return mob
end

function Scene:equip(mob, name, item)
    item = setmetatable(item or {}, { __index = Item[name] })

    -- stack consumables
    local stacked = false
    if item.consume then
        for i = 1, #mob.stuff do
            local thing = mob.stuff[i]
            if thing.name == item.name and thing.consume then
                thing.count = (thing.count or 1) + (item.count or 1)
                item = thing
                stacked = true
                break
            end
        end
    end

    -- add to inventory
    if not stacked then mob.stuff[#mob.stuff + 1] = item end

    -- equip
    if item.slot then mob[item.slot] = item end
end

function Scene:affect(mob, name, effect)
    if not mob.effects then mob.effects = {} end
    effect = setmetatable(effect or {}, { __index = Effect[name] })

    if effect.chance and RNG:random() > effect.chance then return false end

    local stack = effect.stack or 1
    local count = 0
    local i = 1
    while i <= #mob.effects do
        if mob.effects[i].name == effect.name then
            count = count + 1
            if count >= stack then
                table.remove(mob.effects, i).revoke(mob, self.game, count)
                count = count - 1
                i = i - 1
            end
        end
        i = i + 1
    end

    mob.effects[#mob.effects + 1] = effect
    effect.evoke(mob, self.game, count + 1)
    return true
end

function Scene:canMove(mob, x, y)
    if self:getMob(x, y) then
        return false
    end

    if self.player.x == x and self.player.y == y then
        return false
    end

    local f = self:getFeature(x, y)

    return not (f.obstruct or f.obstructFloor)
end


function Scene:canAttack(who, x, y)
    if math.abs(who.x - x) > 1 or math.abs(who.y - y) > 1 then
        return false
    end
    if who == self.player then
        local mob = self:getMob(x, y)
        if mob then
            return mob
        end
    else
        if self.player.x == x and self.player.y == y then
            return self.player
        end
    end
    return false
end

function Scene:passTime()
    if self.passingTime then return end
    self.passingTime = true
    local game, player = self.game, self.player

    local _ = System.affectActive(player, game, self)
        or System.playerCommand(player, game, self)

    while System.delay(player, game, self)
    or System.wait(player, game, self) and System.delay(player, game, self)
    do
        if self.animate > 0 then
            self.animate = self.animate - 1
            local time = os.clock() + 0.025
            game:refresh()
            repeat until os.clock() > time
        end
        game.turns = game.turns + 1
        System.regen(player, game, self)
        System.rest(player, game, self)
        System.affectPassive(player, game, self)

        for _, x, y, mob in self.mob:each() do
            mob.x, mob.y = x, y

            System.regen(mob, game, self)
            System.rest(mob, game, self)
            System.distract(mob, game, self)
            System.affectPassive(mob, game, self)

            _ = System.delay(mob, game, self)
                or System.affectActive(mob, game, self)
                or System.notice(mob, game, self)
                or System.flee(mob, game, self)
                or System.attack(mob, game, self)
                or System.wander(mob, game, self)

            System.death(mob, game, self)
        end

        for _, x, y, cloud in self.cloud:each() do
            cloud.x, cloud.y = x, y

            if cloud.duration and cloud.dissipate and game.rng:random() < cloud.dissipate then
                local f = (game.rng:random() + 3) * 0.25
                local d1, d2 = cloud.duration * f, cloud.duration * (1 - f)
                local vx, vy = game:roll(1, 3) - 2, game:roll(1, 3) - 2
                if self:putCloud(vx + x, vy + y, cloud.name, { duration = d1 }) then
                    cloud.duration = d2
                end
            end

            if cloud.duration then
                cloud.duration = cloud.duration - 1
                if cloud.duration < 1 then self.cloud:setCell(x, y, nil) end
            end

            if cloud.effect then
                local mob = self:getMob(x, y)
                if mob then self:affect(mob, cloud.effect) end
            end

            if cloud.passive then cloud:passive(game) end

        end

    end


    self:computeFov()
    self.passingTime = false
end

local DMG_FACTOR = 0.1

function Scene:attack(attacker, target)
    local weapon = attacker.weapon

    local penalty = math.max(5, weapon.heft - attacker.str)
    attacker.delay = attacker.delay + penalty
    attacker.stamina = attacker.stamina - penalty

    if not target.target then target.target = attacker end

    local evade = self.game:roll(1, 100) + (target.dex + target.dodging - 12) * 3
    local hit = self.game:roll(1, 100) + (attacker.dex + weapon.reach - 12) * 3

    -- dodge
    if evade > hit then
        if attacker == self.player then
            self.game:log(target.definite, ' dodges your attack.')
        else
            self.game:log('You dodge ', attacker.definite, '.')
        end
        return
    end

    local dmg = math.ceil(self.game:roll(attacker.str, weapon.heft) * DMG_FACTOR)
    local armor = target.armor and target.armor.rating or 0

    dmg = math.max(0, dmg - armor)

    local cut
    if dmg > 0 and weapon.blade and weapon.blade > 0 then
        cut = math.ceil((hit - evade) * weapon.blade  * DMG_FACTOR)
        cut = math.max(0, cut - armor)
        dmg = dmg + cut
    end

    target.health = target.health - dmg

    local vi, v2, v3 = math.ceil(dmg / 5), weapon.verb, weapon.verb3

    if attacker == self.player then
        self.game:log('You ',
            dmg == 0 and 'hit' or v2[vi] or v2[#v2],
            ' the ', target.name, ' with your ', weapon.name,
            dmg == 0 and ', but do no damage' or '', '.'
            , hit - evade, ' ', dmg, ' ', cut or 0)
    else
        self.game:log('The ', attacker.name, ' ',
            dmg == 0 and 'hits' or v3[vi] or v3[#v3], ' ',
            target.definite, ' with its ', weapon.name,
            dmg == 0 and ', but does no damage' or '', '.'
            , hit - evade, ' ', dmg, ' ', cut or 0)
    end

    if dmg > 0 and weapon.effect
    and ((not weapon.chance) or (RNG:random() > weapon.chance)) then
        self:affect(target, weapon.effect)
    end

end

function Scene:move(mob, x, y)
    self.mob:setCell(mob.x, mob.y, nil)
    self.mob:setCell(x, y, mob)
    mob.x = x
    mob.y = y
    mob.delay = mob.delay + 10 - mob.speed

    local feature = self:getFeature(x, y)
    if feature.effect then self:affect(mob, feature.effect) end
end

function Scene:movePlayer(vx, vy)
    local player = self.player
    local x = player.x + vx
    local y = player.y + vy

    local target = self:canAttack(player, x, y)
    local feature = self:getFeature(x, y)
    if target then
        self:attack(player, target)
        -- player.delay = player.delay + 1
        -- self:passTime()
    elseif feature.opened then
        self.feature:setCell(x, y, feature.opened)
        player.delay = player.delay + 10 - player.speed
        -- player.delay = player.delay + 1
        -- self:passTime()
    elseif self:canMove(player, x, y) then
        player.x = x
        player.y = y
        player.delay = player.delay + 10 - player.speed
        -- player.delay = player.delay + 1

        if feature.effect then self:affect(player, feature.effect) end
        -- self:passTime()
    end
end

function Scene:computeFov()
    if self.outdoors then
        self.ambientLight[1], self.ambientLight[2], self.ambientLight[3], self.fovRadius =
            self.light:computeDaylight(self.game.turns)
    end
    for i = 1, #self.visibleMobs do
        self.visibleMobs[i].visible = false
    end
    self.field=ROT.Type.Grid()
    self.fov:compute(self.player.x, self.player.y, self.fovRadius, function(x, y, r, v)
        self.field:setCell(x, y, 1)
        self.seen:setCell(x, y, 1)
    end)
    self.visibleMobs = {}
    for _, x, y in self.field:each() do
        self.visibleMobs[#self.visibleMobs + 1] = self:getMob(x, y)
    end
    for i = 1, #self.visibleMobs do
        self.visibleMobs[i].visible = true
    end
end

function Scene:randomFloor(type)
    local x, y
    while true do
        x = RNG:random(1, self.innerWidth)
        y = RNG:random(1, self.innerHeight)
        if self.feature:getCell(x, y) == (type or 'floor') then
            return x, y
        end
    end
end

function Scene:interact()
    local x, y = self.player.x, self.player.y
    -- local act = self.interactive:getCell(x, y)
    -- if act then return act(self) end
    for i = 1, #self.downstairs do
        if x == self.downstairs[i].x and y == self.downstairs[i].y then
            self:descend(i)
            return
        end
    end
    for i = 1, #self.upstairs do
        if x == self.upstairs[i].x and y == self.upstairs[i].y then
            self:ascend(i)
            return
        end
    end
    for i = 1, #self.branches do
        if x == self.branches[i].x and y == self.branches[i].y then
            self:branch(self.branches[i])
            return
        end
    end
end

function Scene:ascend(stairIndex)
    local p
    if self.depth == 1 then
        local b = self.previous.branches
        for i = 1, #b do
            if self == b[i].scene then
                p = b[i]
                break
            end
        end
    else
        p = self.previous.downstairs[stairIndex]
    end
    if p then
        self.game:log('You ascend the stairs.')
        self.game:refresh()
        self.game.scene = self.previous
        self.player.x, self.player.y = p.x, p.y
        -- self.game.scene:passTime(10)
        self.player.delay = self.player.delay + 10
        self.game.scene:computeFov() self.game:refresh()
    end
end

function Scene:descend(stairIndex)
    self.game:log('You descend the stairs.')
    self.game:refresh()-- io.stdout:flush()
    self.next = self.next or getmetatable(self)(self.game, self)
    self.game.scene = self.next
    local p = self.next.upstairs[stairIndex]
    self.player.x, self.player.y = p.x, p.y
    --self.game.scene:passTime(10)
    self.player.delay = self.player.delay + 10
    self.game.scene:computeFov() self.game:refresh()
end

function Scene:branch(branchInfo)
    self.game:log('You descend the stairs.')
    self.game:refresh()-- io.stdout:flush()
    branchInfo.scene = branchInfo.scene
        or require('scene.' .. branchInfo.branch)(self.game, self, 1)
    self.game.scene = branchInfo.scene
    local p = branchInfo.scene.upstairs[1]
    self.player.x, self.player.y = p.x, p.y
    self.player.delay = self.player.delay + 10
    self.game.scene:computeFov() self.game:refresh()
end

return Scene
